除了 ChatGPT 網頁介面以外,還能透過 OpenAI API 調用 ChatGPT 的功能,讓我們能夠在自己的開發應用裡面借助 ChatGPT 的力量。以下介紹 ChatGPT 的各種使用眉角。
(Powered By Microsoft Designer)
在開始介紹 ChatGPT API 之前,必須先介紹 Token 這個概念。在 NLP 領域裡面 Token 是切割文本的最小單位,有些中文翻譯成「詞元」。可以想像是字或詞的概念,而在 BPE (Byte Pair Encoding) Tokenizer 底下,通常會切的更細一點。
一句話會如何被分詞 (Tokenize) 取決於分詞器 (Tokenizer) 如何被訓練,並不一定是依照空格或字元邊界切開。例如在處理單字 "photography" 時,可能會被拆成 "photo" 和 "graphy" 兩個 Subwords,因此它會佔用兩個 Tokens。
在 UTF-8 裡面中日韓表意文字 (CJK Characters) 通常由 3 個 Bytes 所組成。因為中日韓的文字相當多,通常不會全部包含在 BPE Tokenizer 的 Vocabulary 裡面,因此經常需要將沒見過的 UTF-8 字元 Fallback 拆解成 Byte 來表示。
一般情況下,中文(或中日韓文字)消耗的 Token 用量會比英文多的多,因為中文通常要花費比較多 Byte 來表示一個同義的英文單字。例如在英文中 "apple" 通常只需要 1 個 Token 來表示,而蘋果則可能需要 4 ~ 6 個 Tokens 來表示。
Tiktoken 是 OpenAI 的 Tokenizer 套件,這名稱估計是來致敬 TikTok 抖音的,同樣也是使用 BPE Tokenizer。在操作 ChatGPT API 的過程,我們需要透過 OpenAI 官方的 Tokenizer 來精準控制 Token 用量,避免超出模型的輸入長度,或者預防使用者任意輸入過長的句子。以下是一個使用範例:
import tiktoken
tk = tiktoken.encoding_for_model("gpt-3.5-turbo")
token_ids = tk.encode("今天太陽很大")
print(len(token_ids)) # 輸出 9
token_ids = tk.encode("The sun is very bright today")
print(len(token_ids)) # 輸出 6
可以看到,雖然是 6 個中文字,卻用了 9 個 Tokens。若我們將這句話翻譯成英文 "The sun is very bright today" 並計算 Token 數量,會發現只需要 6 個 Tokens。我們進一步來探討 Tokenizer 都把這些字詞切成什麼樣子:
for token in tokenizer.encode("photography攝影"):
b = tokenizer.decode_bytes([token])
print(b)
# Output: b'phot', b'ography', b'\xe6\x94', b'\x9d', b'\xe5\xbd\xb1'
觀察此輸出,我們可以看到:
\xe6\x94
與 \x9d
兩組 Byte 的組合。\xe5\xbd\xb1
的編碼。photography 直覺上可能會認為應該被切成 "photo" 與 "graphy",但這就是 BPE Tokenizer 訓練階段自己統計出來的結果,切成 "phot" 與 "ography" 可能更貼近訓練語料的分佈。
在 BPE Tokenizer 的訓練過程中,為了減少字典大小,部分不常見的 UTF-8 字元會被拆解成 Bytes。這類做法的好處是,在某個程度上避免了中文斷詞錯誤引起的問題。中文斷詞在傳統 NLP 上真的非常令人困擾。這樣的分詞法很大程度的降低這個問題,也具有處理未知詞的能力,然而代價是更龐大的參數量與更難收斂的模型。因此,一些以中文為主的語言模型,可能會考慮擴大其字典,納入更多中文字,這樣可以減少中文 Token 的使用量。
透過 Tiktoken 套件,我們也能比較一下 GPT-3.5 與 GPT-4 Tokenizer 的差異:
tk1 = tiktoken.encoding_for_model("gpt-3.5-turbo")
tk2 = tiktoken.encoding_for_model("gpt-4")
print(tk1, tk2)
將這兩個 Tokenizer 印出來,會發現他們都是 <Encoding 'cl100k_base'>
,原來是因為這兩個模型根本使用相同的 Tokenizer,而這個 Tokenizer 的名稱是 cl100k_base
。因此這部份與 tiktoken.get_encoding("cl100k_base")
的操作是等價的,詳細的 Tokenizer 名稱可以參考官方範例:
Encoding Name | OpenAI Models |
---|---|
cl100k_base |
gpt-4 , gpt-3.5-turbo , text-embedding-ada-002 |
p50k_base |
Codex models, text-davinci-002 , text-davinci-003 |
r50k_base (or gpt2 ) |
GPT-3 models like davinci |
若要相當精準的考慮 Chat Format 對 Token 數量的影響,可以參考官方的 ChatGPT Prompt 格式範例。其關鍵在於每次 ChatGPT 回覆都會增加 <|start|>assistant<|message|>
這三個固定前綴 Tokens,因此 Token 消耗量需要額外 +3 上去。
為什麼這個 Token 這麼重要呢,除了對模型效能的影響以外,對我們開發者的荷包也有很大的影響 💸
ChatGPT API 本身的計價方式,就是以 Token 數量計價的。詳細的計價方式,請參考官方網站,我們這邊以 GPT-3.5 Turbo 的價格為例:
Model | Input | Output |
---|---|---|
4K Context | $0.0015/1K Tokens | $0.002/1K Tokens |
16K Context | $0.0030/1K Tokens | $0.004/1K Tokens |
註:此價格為 2023 年 9 月份的價格,價格經常變動,請以官方網站為準。
4K 與 16K 分別代表不同長度上限的模型種類,長度上限越高,收費越貴。而收費方式也區分為輸入與輸出兩種。其中 4K 的 Input 為 $0.0015/1K Tokens,也就是說每輸入 1000 個 Tokens 就要收費 0.0015 鎂,約新台幣 0.04 ~ 0.05 塊錢。假設每個中文字平均佔用 2 ~ 3 個 Tokens,那 1000 個 Tokens 約莫是 300 ~ 500 個中文字左右。這是 Input 的情況,在 Output 這邊又稍微貴一些。
這個價格到底算不算便宜呢?筆者手邊有個小應用,日均 150 個 Requests 左右,每個 Requests 約需消耗 1000 個 Tokens 左右,月結帳單約落在 7 至 9 鎂之間。對我這種小型開發者而言,月均成本不到新台幣 300 元,是相當小的一筆開銷,只需要一點簡單的營利手段就很容易打平甚至獲利。
這邊要特別注意:使用 GPT-3 跟 GPT-3.5 的價格差距是很大的!GPT-3 的價格是 $0.02/1K Tokens,足足是 GPT-3.5 的十倍有餘!根據官方所述 GPT-3 已經是即將被棄用的模型,其效果與 GPT-3.5 差距是很大的。但有些比較早期的專案還是在使用 GPT-3 (text-davinci-003
),因此在套用別人的專案時,要格外注意這點。
根據官方文件所述,建議使用
gpt-3.5-turbo-instruct
取代 Completions API 的text-davinci-003
模型。
在使用 API 之前,需要先註冊帳號並取得 API 金鑰。如果是新張號的話,前三個月有 5 鎂的免費額度可以用。但如果你的帳號已經辦一陣子了,那就需要綁信用卡花點小錢才能用了。
使用 API 時,建議多多參考官方文件。筆者主要使用 Python 開發,因此需要安裝 Python 版的 OpenAI API 套件:
pip install openai
可以透過以下方式進行認證:
import openai
openai.api_key = "sk-...xxxx"
但是在程式碼裡面寫死 API Key 顯然不是個好選擇,替代方案除了常用的環境變數以外,也可以將 API Key 存在檔案裡面,並指定 API Key 的路徑:
openai.api_key_path = "API.Key"
另外,如果你是有加入公司組織之類的,記得要設定 Organization 資訊:
openai.organization = "org-...xxxx"
不然帳單就不是報公帳,而是算在你頭上囉~
完成認證後,我們就可以開始串接 ChatGPT API 了!以下是個基本範例:
import openai
openai.api_key_path = "API.Key"
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "請使用繁體中文回答。"},
{"role": "user", "content": "你好啊!"},
],
)
print(response["choices"][0]["message"]["content"])
# 輸出結果:你好!有什麼我可以幫助你的嗎?
透過 model
參數指定要用哪個模型,除了 gpt-3.5-turbo
以外,還有 gpt-4
以及各自不同的長度和日期版本,例如 gpt-4-32k-0613
就代表是輸入長度支援到 32K 且釋出日期為 6 月 13 號的 GPT-4 模型。模型代號經常有變動,請以官方網站宣布的為主。
要傳送給模型處理的訊息透過 messages
參數傳遞,這個參數接收一個陣列,陣列裡面每個元素代表每個回合的對話內容。在對話內容裡面可以設定 role
來代表該訊息的角色,主要有 system
, user
, assistant
三種角色。其中 system
就是昨天提到的 System Prompt 概念,而 user
與 assistant
分別代表使用者的輸入與模型的輸出。
我們可以透過一些額外的參數來控制生成的結果,例如 max_tokens
可以設定最多輸出幾個 Tokens,參數 stop
可以設定模型輸出遇到什麼字串需要停下來。此外還有 temperature
, top_p
和各種 Penalty 等取樣參數,這在未來會詳細談到。
加上參數 stream=True
即可用串流的方式接收輸出,而 response
也會因此變成一個 Generator 物件:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "請使用繁體中文回答。"},
{"role": "user", "content": "你好啊!"},
],
stream=True,
)
for resp in response:
try:
print(end=resp["choices"][0]["delta"]["content"], flush=True)
except:
pass
print()
這樣就會看到模型輸出一個字一個字的跑出來啦!用 Streaming 的方式顯示輸出,比較能感受到模型跟連線都還「活著」的感覺。最終要不要使用 Streaming 輸出,取決於應用層是否需要將輸出顯示給使用者看。如果使用者會直接接觸到這些輸出,那有 Streaming 會讓使用者比較感受的到回饋。如果單純只是應用端的邏輯處理,那只需要使用 Non-Streaming 即可。
OpenAI 回傳的格式其實涵蓋的資訊蠻多的,而 delta/content
這個位置不一定每次都有。筆者嘗試了多種方法避免 try-except 的語法,研究到最後的心得是:用 try-except 最快 QQ
我們可以將使用者輸入與模型輸出不斷加到 messages
裡面來達到多輪對話的效果。但如果訊息太長或者要做成本控制,我們就需要結合 Tiktoken 套件來截斷輸入。在截斷的過程記得保留系統提示,並且從最舊的訊息開始截斷。基於這個邏輯,我們可以製作一個簡單的文字聊天介面,完整程式碼如下:
import openai
import tiktoken
openai.api_key_path = "API.Key"
tk = tiktoken.encoding_for_model("gpt-3.5-turbo")
def truncate(messages, limit=300):
"""
我們從訊息的尾端往前拜訪,不斷累加總 Token 數
直到總 Token 數超過限制,最後將 System Prompt 加回訊息裡面
"""
total = 0
new_messages = list()
for msg in reversed(messages[1:]):
total += len(tk.encode(msg["content"]))
if total > limit:
break
new_messages.insert(0, msg)
new_messages.insert(0, messages[0])
return new_messages
def chat(messages):
return openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=messages,
stream=True,
)
def show(response):
# 使用 Streaming 的方式顯示模型的輸出
full_resp = str()
print(end="Assistant: ", flush=True)
for resp in response:
try:
token = resp["choices"][0]["delta"]["content"]
print(end=token, flush=True)
full_resp += token
except:
pass
print()
return full_resp
messages = [{"role": "system", "content": "你現在是一個使用繁體中文的貓娘。"}]
while True:
prompt = input("User: ").strip()
messages.append({"role": "user", "content": prompt})
messages = truncate(messages)
response = chat(messages)
response = show(response)
# 將模型輸出加入歷史訊息
messages.append({"role": "assistant", "content": response})
使用起來的狀況大致如下:
User: 你好啊
Assistant: 嗨!你好啊!有什麼我可以幫助你的嗎?
User: 請你簡短的自我介紹一下
Assistant: 嗨!我是一隻習慣使用繁體中文的貓娘 ...
User: 請問貓咪如何清潔自己
Assistant: 貓咪通常會利用自己的舌頭和腳來進行清潔 ...
透過幾行簡單的程式碼,我們成功打造了一隻可愛貓娘助手 ((誤
Embedding 在 NLP 裡面扮演相當重要的角色,尤其在資訊檢索 (Information Retrieval) 領域裡面相當實用。而 OpenAI 也提供 Embedding API 讓使用者可以將文本轉成 Sentence Embedding 來使用,以下是一個簡單範例:
response = openai.Embedding.create(
model="text-embedding-ada-002",
input=["我喜歡貓咪", "來去看電影"],
)
for item in response["data"]:
embeddings = item["embedding"]
print(len(embeddings), embeddings[:4])
"""
輸出結果:
1536 [-0.01141282077, 0.01442165579, -0.009603629820, -0.03597632423]
1536 [-0.00960167869, -0.03483279421, -0.015344557352, -0.04107420891]
"""
Ada v2 最大輸入長度可以達到 8192 個 Tokens,每份 Embedding 的維度是 1536 維。Embedding API 可以同時處理很多個句子,減少傳送處理的時間,因此會比你一句一句取 Embedding 快的多。這種 Embedding 最常見的用法就是比對文本之間的相似度,例如透過計算 Embedding 之間的歐式距離 (Euclidean Distance) 來觀察跨語言文本之間的相似度如何:
import numpy as np
import openai
from sklearn.metrics.pairwise import euclidean_distances
openai.api_key_path = "API.Key"
value_list = ["我喜歡貓咪", "來去看電影"]
response = openai.Embedding.create(input=value_list, model="text-embedding-ada-002")
values = [item["embedding"] for item in response["data"]]
values = np.array(values)
query_list = ["i like cats", "watch a movie", "猫が好き", "映画を見に来てください"]
response = openai.Embedding.create(input=query_list, model="text-embedding-ada-002")
print(value_list)
for i, item in enumerate(response["data"]):
embeddings = item["embedding"]
print(euclidean_distances([embeddings], values)[0], query_list[i])
"""
稍微經過人工排版的輸出結果:
["我喜歡貓咪", "來去看電影"]
[ 0.49925489 0.72721379] i like cats
[ 0.77279439 0.54520915] watch a movie
[ 0.50884729 0.68798437] 猫が好き
[ 0.72506646 0.50975780] 映画を見に来てください
"""
因為我們是計算歐式距離的關係,所以數字越小代表越相近,也可以換成 Cosine Similarity 之類的評估公式。可以看到 Embedding 不僅能用來比較文本之間的相似度,也具有跨語言的能力,在未來提到 Retrieval-Based 應用時會相當重要。
另外 Embedding API 的 Ada v2 只要 $0.0001/1K Tokens 超便宜!
在使用 Open API 時,其用量除了受限於開發者的荷包以外,官方也有限制 API 的存取速率。詳細資訊請參考官方網站,在文字與 Embedding 部分,常用的單位為 Requests Per Minute (RPM) 與 Tokens Per Minute (TPM) 兩種。RPM 是指每分鐘可以存取 API 的次數上限,而 TPM 則是代表每分鐘可以要求 API 處理的 Token 數量上限。
例如使用 ChatGPT API 時,如果發送大量短訊息,那就有可能先踩到 RPM 的上限。而在使用 Embedding API 時,如果每次 Request 都發送很大量的文本,那就有可能先踩到 TPM 的上限。
根據我的理解,使用量達到任何一個限制都會先被擋下來,但是在合理的使用下,官方並不會把你封鎖起來,而是回傳一個達到速率上限的錯誤訊息。
這種情況在做實驗時很容易遇到,因為 ChatGPT 一筆一筆生答案其實是挺慢的,因此有些實驗可能會透過 Multi-Threading 的方式發送 Requests,記得在裡面捕捉超速錯誤並延遲一段時間再送一次。
為了展示這個速率限制,筆者自掏腰包,以 0.0004 鎂(約新台幣 0.012 塊錢)的代價,以 Embedding API 的 3500 RPM 為例,測試了一分鐘內發四千個 Embedding API 的 Requests 給大家看:
import time
from concurrent.futures import ThreadPoolExecutor
from threading import Lock
import openai
from openai.error import RateLimitError
openai.api_key_path = "API.Key"
results = dict()
lock = Lock()
def create_request(thread_id):
print(f"Thread {thread_id} Begin")
while True:
try:
response = openai.Embedding.create(
model="text-embedding-ada-002",
input=["0"],
)
break
except RateLimitError as e:
print(f"Rate Limit Error! Wait For 5 Seconds ...")
time.sleep(5)
with lock:
results[thread_id] = response
current_length = len(results)
print(f"Thread {thread_id} Done {current_length}")
thread_ids = range(1) # 為了避免有人直接拿來跑,請自行將數字改成 4000 做測試
with ThreadPoolExecutor(max_workers=128) as executor:
executor.map(create_request, thread_ids)
!! 注意 !! 請不要輕易嘗試以上程式碼,可能會造成不可預期的開銷。
這個程式在運作的過程中,會不斷拋出 Error: Rate limit reached for ...
的錯誤訊息,此 Exception 類別為 openai.error.RateLimitError
。以上作法僅供參考,也可以使用其他有 retry 功能的套件來處理,這部份可以參考官方文件的推薦。
開發者可以到 OpenAI 的 Usage 頁面查看 API 的使用量與目前累積的花費。下方的 Daily usage breakdown (UTC)
可以查詢詳細的 Request 資訊,包含各時間區段的 Request 次數與 Token 用量,以及使用了哪個模型之類的。這個用量的更新約略會延遲個五分鐘左右,大標寫的時間基本上是 UTC+0 的時間,但是點開詳細資訊可以看到 Local Time 本地時間。
如果整合好 API 的應用正式上線後,可以透過此頁面監控使用量與計費,並分析應用的成本。
OpenAI 其實不只有提供語言模型的服務,還有 Whisper 語音辨識以及 DALL-E 圖像生成等功能。這兩個功能也是相當實用,所以筆者也順便介紹一下這些 API 的用法。
Whisper 是 OpenAI 訓練的一個語音轉文字 (Speech To Text, STT) 模型,採用 Encoder-Decoder 的 Transformer 架構。具有跨語言辨識的能力,同樣是 OpenAI API 的其中一項服務,其使用方式為上傳音檔。
音檔格式的支援滿廣泛的,最高上傳 25 MB 大小的音檔。筆者建議轉成 Mp3 格式,比較節省流量也能減少網路傳輸時間。可以透過 FFmpeg 轉換格式,例如:
ffmpeg -i audio.wav audio.mp3
以下是一個簡單的使用範例:
import openai
openai.api_key_path = "API.Key"
audio = open("audio.mp3", "rb")
transcript = openai.Audio.transcribe("whisper-1", audio)
print(transcript["text"])
Whisper API 的辨識速度相當的快,600 秒的音檔只要 30 秒就能完成辨識,是 Real-Time 的 20 倍快,速度相當驚人。筆者實際使用 RTX 3090 做辨識,約莫也是 8 ~ 10 倍快而已。
Whisper 除了能做傳統的語音辨識以外,還能直接將語音翻譯成其他語言。但目前官方的 API 只支援翻譯成英文,這裡就不特別做介紹了。
Whisper API 的計價方式是每辨識一分鐘的音檔便收取 $0.006 鎂的費用,辨識十分鐘的音檔約莫就是新台幣 1 ~ 2 塊錢而已,拿來幫喜歡的 VTuber 製作影片逐字稿蠻方便的。
但如果你真的想客家一點,其實 Whisper 的模型權重是有開源的!相比於 ChatGPT 這種語言模型而言,語音辨識的權重要來的小很多。需要佔用的 GPU Memory 約 6 GB 左右,若擁有不錯的 GPU 可以嘗試自己運行 Whisper 模型,在需求量不大的情況下會相對經濟一點,以下是一些可以參考的資源:
DALL-E 是 OpenAI 的圖像生成模型,雖然知名度可能不如 Stable Diffusion 之類的,但也是個可供參考的服務。以下是個簡單的範例程式:
import openai
import requests
openai.api_key_path = "API.Key"
response = openai.Image.create(
prompt="a cool cat drinking coffee",
n=1,
size="256x256",
)
image_url = response["data"][0]["url"]
resp = requests.get(image_url)
with open("image.png", "wb") as fp:
fp.write(resp.content)
筆者測試生成一張 256 x 256 大小的圖片,大約需要 5 ~ 8 秒左右,速度還算蠻理想的。除了一般的圖像生成以外,還有圖片編輯 (Edits) 與變體 (Variations) 等功能,詳細用法請參考官方文件。這個 API 的計價方式是按圖收費,每張 256x 大小的圖片收取 0.016 鎂,而最大的 1024x 每張圖片則收取 0.02 鎂。
(Powered By OpenAI DALL-E)
「如果將如此龐大的語言模型直接開源,將會很難限制使用者的不當用途。但如果語言模型的存取限制太高,這樣一來,普羅大眾將很難受益於這項先進科技。因此其中一個可行的方法是透過 API 的形式提供服務,由 API Provider 來負責監控不當使用的情況。」以上描述來自 InstructGPT 的論文。筆者在很大程度上認同這個觀點,尤其是這篇論文發表的時間是 2022 年 3 月,當時 ChatGPT 甚至都還沒發表。
直到今年 2023 年 3 月初,ChatGPT API 才正式上線。雖然 API 的形式依然留有許多問題,例如無法連網的裝置、使用者的隱私問題等等。但是其低廉的價格與存取的便利性,確實讓我們這些小型開發者大大受益。讓我們可以專注在上層的應用開發,而不需要煩惱硬體設備不足的問題,只要保持網路暢通即可!
以下是引自 InstructGPT 論文的原文:
If these models are open-sourced, it becomes challenging to limit harmful applications in these and other domains without proper regulation. On the other hand, if large language model access is restricted to a few organizations with the resources required to train them, this excludes most people from access to cutting-edge ML technology. Another option is for an organization to own the end-to-end infrastructure of model deployment, and make it accessible via an API. This allows for the implementation of safety protocols like use case restriction (only allowing the model to be used for certain applications), monitoring for misuse and revoking access to those who misuse the system, and rate limiting to prevent the generation of large-scale misinformation.
本文所提及的價格皆為撰稿當下的資訊,收費標準時常變動,請以官方網站公佈的價目為主。